/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.lvyh.lightframe.core.invoke;


import com.lvyh.lightframe.core.provider.ProviderInfo;

import java.net.InetSocketAddress;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Based on ThreadLocal context passing, it is mainly used for internal use.
 * Generally, it exists in client request thread, server business thread pool and client asynchronous thread.
 */
public class RpcInternalContext implements Cloneable {


    /**
     * The constant LOCAL.
     */
    private static final ThreadLocal<RpcInternalContext> LOCAL = new ThreadLocal<RpcInternalContext>();

    /**
     * The constant DEQUE_LOCAL.
     */
    private static final ThreadLocal<Deque<RpcInternalContext>> DEQUE_LOCAL = new ThreadLocal<Deque<RpcInternalContext>>();

    /**
     * Attached property
     */
    private Map<String, Object> attachments = new ConcurrentHashMap<String, Object>();

    /**
     * Set context
     */
    public static void setContext(RpcInternalContext context) {
        LOCAL.set(context);
    }

    /**
     * Get context, if empty, create automatically
     */
    public static RpcInternalContext getContext() {
        RpcInternalContext context = LOCAL.get();
        if (context == null) {
            context = new RpcInternalContext();
            LOCAL.set(context);
        }
        return context;
    }

    /**
     * View context
     */
    public static RpcInternalContext peekContext() {
        return LOCAL.get();
    }

    /**
     * Clean up context
     */
    public static void removeContext() {
        LOCAL.remove();
    }

    /**
     * Put the context down one layer (for example, server B receives a request from a and then calls it as client of C. before calling, it stores the upper and lower files of a-b)
     */
    public static void pushContext() {
        RpcInternalContext context = LOCAL.get();
        if (context != null) {
            Deque<RpcInternalContext> deque = DEQUE_LOCAL.get();
            if (deque == null) {
                deque = new ArrayDeque<RpcInternalContext>();
                DEQUE_LOCAL.set(deque);
            }
            deque.push(context);
            LOCAL.set(null);
        }
    }

    /**
     * Take the context one layer up (for example, after receiving the request from a, server B calls it as client of C. after the call, the context of A-B is taken up first.)
     */
    public static void popContext() {
        Deque<RpcInternalContext> deque = DEQUE_LOCAL.get();
        if (deque != null) {
            RpcInternalContext context = deque.peek();
            if (context != null) {
                LOCAL.set(deque.pop());
            }
        }
    }

    /**
     * Clean up all contexts
     */
    public static void removeAllContext() {
        LOCAL.remove();
        DEQUE_LOCAL.remove();
    }


    /**
     * Instantiates a new Rpc context.
     */
    protected RpcInternalContext() {
    }


    /**
     * The Local address.
     */
    private InetSocketAddress localAddress;

    /**
     * The Remote address.
     */
    private InetSocketAddress remoteAddress;


    /**
     * The Provider side.
     */
    private Boolean providerSide;

    /**
     * Server information to call
     */
    private ProviderInfo providerInfo;

    /**
     * Is provider side.
     *
     * @return the boolean
     */
    public boolean isProviderSide() {
        return providerSide != null && providerSide;
    }

    /**
     * Sets provider side.
     *
     * @param isProviderSide the is provider side
     * @return the provider side
     */
    public RpcInternalContext setProviderSide(Boolean isProviderSide) {
        this.providerSide = isProviderSide;
        return this;
    }

    /**
     * Is consumer side.
     *
     * @return the boolean
     */
    public boolean isConsumerSide() {
        return providerSide != null && !providerSide;
    }

    /**
     * set local address.
     *
     * @param address the address
     * @return context local address
     */
    public RpcInternalContext setLocalAddress(InetSocketAddress address) {
        this.localAddress = address;
        return this;
    }

    /**
     * set local address.
     *
     * @param host the host
     * @param port the port
     * @return context local address
     */
    @Deprecated
    public RpcInternalContext setLocalAddress(String host, int port) {
        if (host == null) {
            return this;
        }
        if (port < 0 || port > 0xFFFF) {
            port = 0;
        }
        this.localAddress = InetSocketAddress.createUnresolved(host, port);
        return this;
    }

    /**
     * @return local address
     */
    public InetSocketAddress getLocalAddress() {
        return localAddress;
    }

    /**
     * set remote address.
     *
     * @param address the address
     * @return context remote address
     */
    public RpcInternalContext setRemoteAddress(InetSocketAddress address) {
        this.remoteAddress = address;
        return this;
    }

    /**
     * set remote address.
     *
     * @param host the host
     * @param port the port
     * @return context remote address
     */
    public RpcInternalContext setRemoteAddress(String host, int port) {
        if (host == null) {
            return this;
        }
        if (port < 0 || port > 0xFFFF) {
            port = 0;
        }
        this.remoteAddress = InetSocketAddress.createUnresolved(host, port);
        return this;
    }

    /**
     * @return remote address
     */
    public InetSocketAddress getRemoteAddress() {
        return remoteAddress;
    }

    /**
     * set attachment.
     *
     * @param key   the key
     * @param value the value
     * @return context attachment
     */
    public RpcInternalContext setAttachment(String key, Object value) {
        if (key == null) {
            return this;
        }
        if (value == null) {
            attachments.remove(key);
        } else {
            attachments.put(key, value);
        }
        return this;
    }

    /**
     * remove attachment.
     *
     * @param key the key
     * @return Old value
     */
    public Object removeAttachment(String key) {
        return attachments.remove(key);
    }

    /**
     * get attachments.
     *
     * @return attachments attachments
     */
    public Map<String, Object> getAttachments() {
        return attachments;
    }

    /**
     * Sets provider info.
     *
     * @param providerInfo the provider info
     * @return the provider info
     */
    public RpcInternalContext setProviderInfo(ProviderInfo providerInfo) {
        this.providerInfo = providerInfo;
        return this;
    }

    /**
     * Gets provider info.
     *
     * @return the provider info
     */
    public ProviderInfo getProviderInfo() {
        return providerInfo;
    }

    @Override
    public String toString() {
        return super.toString() + "{" +
                ", localAddress=" + localAddress +
                ", remoteAddress=" + remoteAddress +
                ", providerSide=" + providerSide +
                ", providerInfo=" + providerInfo +
                '}';
    }

    @Override
    public RpcInternalContext clone() {
        try {
            return (RpcInternalContext) super.clone();
        } catch (Exception e) {
            RpcInternalContext context = new RpcInternalContext();
            context.localAddress = this.localAddress;
            context.remoteAddress = this.remoteAddress;
            context.providerSide = this.providerSide;
            context.providerInfo = this.providerInfo;
            return context;
        }
    }

}